/* main.vala
 *
 * Copyright (C) 2010, 2011  Richard Talbot-Watkins
 * Copyright (C) 2010, 2011  Dave Jeffery
 * 
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * Authors:
 *  Richard Talbot-Watkins <richtw1@gmail.com>
 * 	Dave Jeffery <david.richard.jeffery@gmail.com>
 */

using Gtk;
using Cairo;

/**
 * The main window class for the Monster Designer.
 *
 * The Vala bindings for the menu radio item classes are faulty so this  
 * code, which works around the problems, is liable to break if and when  
 * these bindings are fixed.
 *
 * @author Richard Talbot-Watkins <richtw1@gmail.com>
 * @author Dave Jeffery <david.richard.jeffery@gmail.com>
 * @version 2010.1203
 * @since 0.1
 */
public class MonsterDesigner.MainForm : Window {
	private HBox hbox;
	private DrawingTools tool_box;
	private CheckMenuItem view_tool_box; 
	private ShowColourPanel show_colour_panel;
	private ColourPanel colour_0_panel;
	private ColourPanel colour_1_panel;
	private ColourPanel colour_2_panel;
	private ColourPanel colour_3_panel;
	private PreviewImage preview_image;
	private EditorPanel editor_panel;
	private MenuItem file_save;
	private MenuItem file_save_as;
	private MenuItem file_close;
	private MenuItem edit_menu_button;
	private MenuItem select_menu_button;
	private MenuItem mode_menu_button;
	private MenuItem view_menu_button;
	private CheckMenuItem view_grid;
	private SpinButton spin_button;
	private RadioMenuItem mode_draw;
	private RadioMenuItem mode_fill;
	private RadioMenuItem mode_insert;
	private RadioMenuItem mode_delete;
	private RadioMenuItem mode_select;
	private RadioMenuItem mode_paste;

	/**
	 * Sprite data for monster designer
	 */
	private SpriteData sprite_data = new SpriteData ();

	/**
	 * Palette data for monster designer
	 */
	private PaletteData palette_data = new PaletteData ();

	/**
	 * Stores the colours used for drawing and erasing.
	 */
	private PaintColours paint_colours = new PaintColours ();

	/**
	 * Stores the status of the grid in the editor widget.
	 */
	private GridState grid_state = new GridState ();

	/**
	 * Stores currently selected editing mode.
	 */
	private ToolBoxMode edit_mode = new ToolBoxMode ();

	/**
	 * File object containing the user's data directory e.g. ~/Documents.
	 */
	private File file = FileUtilities.get_user_data_directory ();

	/**
	 * Redo/undo history.
	 */
	private History history;

	/**
	 * Set when there is anything pending which should be saved.
	 */
	private bool _is_unsaved = false;        

	/**
	 * Getter/Setter for the is_unsaved property. 
	 *
	 * The setter updates the title bar.
	 */
	private bool is_unsaved {
		get { 
			return _is_unsaved; 
		}
		set {   
			bool old_state = _is_unsaved;
            _is_unsaved = value;

			if (old_state != _is_unsaved) {
				set_form_title ();
			}
		}
	}

	/**
	 * Set to true when the no file is currently open in the editor.
	 */
	private bool is_closed = true;

	/**
	 * Constructor for the main window.
	 */
	public MainForm () {

		set_form_title ();
		set_default_size (531, 662);

		this.icon = FileUtilities.load_icon ("monster-designer", 16);

		var accelerator_group = new AccelGroup ();
		add_accel_group (accelerator_group);

		var menu_bar = new MenuBar ();

		var file_menu = new Menu ();
		var file_new = new MenuItem.with_mnemonic ("_New");
		var file_open = new MenuItem.with_mnemonic ("_Open...");
		file_save = new MenuItem.with_mnemonic ("_Save");
		file_save_as = new MenuItem.with_mnemonic ("_Save As...");
		file_close = new MenuItem.with_mnemonic ("_Close");
		var file_quit = new MenuItem.with_mnemonic ("_Quit");
		file_menu.append (file_new);
		file_menu.append (file_open);
		file_menu.append (new SeparatorMenuItem ());
		file_menu.append (file_save);
		file_menu.append (file_save_as);
		file_menu.append (new SeparatorMenuItem ());
		file_menu.append (file_close);
		file_menu.append (file_quit);

		var file_menu_button = new MenuItem.with_mnemonic ("_File");
		file_menu_button.set_submenu (file_menu);

		file_new.add_accelerator ("activate", 
									accelerator_group,
									Gdk.keyval_from_name ("N"),
									Gdk.ModifierType.CONTROL_MASK,
									AccelFlags.VISIBLE);

		file_open.add_accelerator ("activate", 
									accelerator_group,
									Gdk.keyval_from_name ("O"),
									Gdk.ModifierType.CONTROL_MASK,
									AccelFlags.VISIBLE);

		file_save.add_accelerator ("activate", 
									accelerator_group,
									Gdk.keyval_from_name ("S"),
									Gdk.ModifierType.CONTROL_MASK,
									AccelFlags.VISIBLE);

		file_save_as.add_accelerator ("activate", 
										accelerator_group,
										Gdk.keyval_from_name ("S"),
										Gdk.ModifierType.CONTROL_MASK |
										Gdk.ModifierType.SHIFT_MASK,
										AccelFlags.VISIBLE);

		file_close.add_accelerator ("activate", 
									accelerator_group,
									Gdk.keyval_from_name ("W"),
									Gdk.ModifierType.CONTROL_MASK,
									AccelFlags.VISIBLE);

		file_quit.add_accelerator ("activate", 
									accelerator_group,
									Gdk.keyval_from_name ("Q"),
									Gdk.ModifierType.CONTROL_MASK,
									AccelFlags.VISIBLE);

		file_new.activate.connect (on_new_activated);
		file_open.activate.connect (on_open_activated);
		file_save.activate.connect (on_save_activated);
		file_save_as.activate.connect (on_save_as_activated);
		file_close.activate.connect (on_close_activated);
		file_quit.activate.connect (on_file_quit_activated);

		menu_bar.append (file_menu_button);

		var edit_menu = new Menu ();
		var edit_undo = new MenuItem.with_label ("Undo");           
		var edit_redo = new MenuItem.with_label ("Redo");
		var edit_cut = new MenuItem.with_label ("Cut");
		var edit_copy = new MenuItem.with_label ("Copy");
		var edit_paste = new MenuItem.with_mnemonic ("_Paste");
		edit_menu.append (edit_undo); 
		edit_menu.append (edit_redo);
		edit_menu.append (new SeparatorMenuItem ());
		edit_menu.append (edit_cut);
		edit_menu.append (edit_copy);
		edit_menu.append (edit_paste);

		edit_menu_button = new MenuItem.with_mnemonic ("_Edit");
		edit_menu_button.set_submenu (edit_menu);

		edit_undo.add_accelerator ("activate", 
									accelerator_group,
									Gdk.keyval_from_name ("Z"),
									Gdk.ModifierType.CONTROL_MASK,
									AccelFlags.VISIBLE);

		edit_redo.add_accelerator ("activate", 
									accelerator_group,
									Gdk.keyval_from_name ("Z"),
									Gdk.ModifierType.CONTROL_MASK |
									Gdk.ModifierType.SHIFT_MASK,
									AccelFlags.VISIBLE);

		edit_cut.add_accelerator ("activate", 
									accelerator_group,
									Gdk.keyval_from_name ("X"),
									Gdk.ModifierType.CONTROL_MASK,
									AccelFlags.VISIBLE);

		edit_copy.add_accelerator ("activate", 
									accelerator_group,
									Gdk.keyval_from_name ("C"),
									Gdk.ModifierType.CONTROL_MASK,
									AccelFlags.VISIBLE);

		edit_paste.add_accelerator ("activate", 
                                  	accelerator_group,
									Gdk.keyval_from_name ("V"),
									Gdk.ModifierType.CONTROL_MASK,
									AccelFlags.VISIBLE);

		/* FIXME Grey out undo/redo if no history. */
		edit_undo.activate.connect (on_undo_activated);
		edit_redo.activate.connect (on_redo_activated);
		edit_copy.activate.connect (on_copy_activated);
		edit_paste.activate.connect (on_paste_activated);

		menu_bar.append (edit_menu_button);

		var select_menu = new Menu ();
		var select_all = new MenuItem.with_label ("All");
		var select_none = new MenuItem.with_label ("None");
		var select_flip_horizontal = new MenuItem.with_label 
												("Flip Horizontally");
		var select_flip_vertical = new MenuItem.with_label 
												("Flip Vertically");
		select_menu.append (select_all);
		select_menu.append (select_none);
		select_menu.append (new SeparatorMenuItem ());
		select_menu.append (select_flip_horizontal);
		select_menu.append (select_flip_vertical);

		select_all.add_accelerator ("activate", 
									accelerator_group,
									Gdk.keyval_from_name ("A"),
									Gdk.ModifierType.CONTROL_MASK,
									AccelFlags.VISIBLE);

		select_none.add_accelerator ("activate", 
										accelerator_group,
										Gdk.keyval_from_name ("A"),
										Gdk.ModifierType.CONTROL_MASK |
										Gdk.ModifierType.SHIFT_MASK,
										AccelFlags.VISIBLE);

		select_menu_button = new MenuItem.with_mnemonic ("_Select");
		select_menu_button.set_submenu (select_menu);

		select_all.activate.connect (on_select_all_activated);
		select_none.activate.connect (on_select_none_activated);
		select_flip_horizontal.activate.connect (on_flip_horizontal_activated);
		select_flip_vertical.activate.connect (on_flip_vertical_activated);		

		menu_bar.append (select_menu_button);

		var mode_menu = new Menu ();	
		var mode_group = new SList<RadioMenuItem> ();

		mode_draw = new RadioMenuItem.with_mnemonic(
									mode_group, "_Draw");
		mode_fill = new RadioMenuItem.with_mnemonic (
									mode_draw.get_group (), "_Fill");
		mode_insert = new RadioMenuItem.with_mnemonic (
									mode_draw.get_group (), "_Insert Row");
		mode_delete = new RadioMenuItem.with_mnemonic (
									mode_draw.get_group (), "Delete _Row");
		mode_select = new RadioMenuItem.with_mnemonic (
									mode_draw.get_group (), "_Select");
		mode_paste = new RadioMenuItem.with_mnemonic (
									mode_draw.get_group (), "_Paste");
		mode_menu.append (mode_draw);
		mode_menu.append (mode_fill);
		mode_menu.append (mode_insert);
		mode_menu.append (mode_delete);
		mode_menu.append (mode_select);
		mode_menu.append (mode_paste);

		mode_draw.add_accelerator ("activate", 
									accelerator_group,
									Gdk.keyval_from_name ("D"),
									0,
									AccelFlags.VISIBLE);

		mode_fill.add_accelerator ("activate", 
									accelerator_group,
									Gdk.keyval_from_name ("F"),
									0,
									AccelFlags.VISIBLE);

		mode_insert.add_accelerator ("activate", 
									accelerator_group,
									Gdk.keyval_from_name ("I"),
									0,
									AccelFlags.VISIBLE);

		mode_delete.add_accelerator ("activate", 
									accelerator_group,
									Gdk.keyval_from_name ("R"),
									0,
									AccelFlags.VISIBLE);

		mode_select.add_accelerator ("activate", 
									accelerator_group,
									Gdk.keyval_from_name ("S"),
									0,
									AccelFlags.VISIBLE);

		mode_paste.add_accelerator ("activate", 
									accelerator_group,
									Gdk.keyval_from_name ("P"),
									0,
									AccelFlags.VISIBLE);

		mode_menu_button = new MenuItem.with_mnemonic ("_Mode");
		mode_menu_button.set_submenu (mode_menu);
		menu_bar.append (mode_menu_button);

		/* Connect the toolbox buttons to handlers */
		mode_draw.toggled.connect (on_draw_radio_toggled);
		mode_fill.toggled.connect (on_fill_radio_toggled);
		mode_insert.toggled.connect (on_insert_radio_toggled);
		mode_delete.toggled.connect (on_delete_radio_toggled);
		mode_select.toggled.connect (on_select_radio_toggled);
		mode_paste.toggled.connect (on_paste_radio_toggled);

		var view_menu = new Menu ();
		view_grid = new CheckMenuItem.with_label ("Show Grid");
		view_grid.active = grid_state.enabled;          
		view_grid.tooltip_markup = "Display the image's grid";
		view_menu.append (view_grid);
		view_grid.toggled.connect (on_grid_activated);
        
		view_tool_box = new CheckMenuItem.with_label ("Show Tool Box");
		view_tool_box.active = true;
		view_tool_box.tooltip_markup = "Display the tool box";
		view_menu.append (view_tool_box);

		view_tool_box.activate.connect (on_tool_box_activated);

		view_menu_button = new MenuItem.with_mnemonic ("_View");
		view_menu_button.set_submenu (view_menu);
		menu_bar.append (view_menu_button);

		var help_menu = new Menu ();
		var help_about = new MenuItem.with_label ("About");

		help_about.activate.connect (on_about_activated);
        
		help_menu.append(help_about);
        
		var help_menu_button = new MenuItem.with_mnemonic ("_Help");
		help_menu_button.set_submenu (help_menu);
		menu_bar.append (help_menu_button);

		hbox = new HBox (false, 0);

		var show_colour_frame = new Frame (null);
		show_colour_frame.shadow_type = ShadowType.ETCHED_OUT;

		show_colour_panel = new ShowColourPanel (paint_colours, palette_data);
		show_colour_panel.tooltip_markup = "Colour indicator.\n" +
											"Draw colour shown top left.\n" +
											"Erase colour shown bottom right.";

		show_colour_frame.add (show_colour_panel);
  
		var colour_vbox = new VBox (false, 4);          

		var colour_0_alignment = new Alignment (0.5f, 0.5f, 0.0f, 1.0f);
		var colour_1_alignment = new Alignment (0.5f, 0.5f, 0.0f, 1.0f);
		var colour_2_alignment = new Alignment (0.5f, 0.5f, 0.0f, 1.0f);
		var colour_3_alignment = new Alignment (0.5f, 0.5f, 0.0f, 1.0f);

		var colour_0_hbox = new HBox (false, 0);
		var colour_1_hbox = new HBox (false, 0);
		var colour_2_hbox = new HBox (false, 0);
		var colour_3_hbox = new HBox (false, 0);

		var frame_0 = new Frame (null);
		frame_0.shadow_type = ShadowType.ETCHED_OUT;
		var frame_1 = new Frame (null);
		frame_1.shadow_type = ShadowType.ETCHED_OUT;
		var frame_2 = new Frame (null);
		frame_2.shadow_type = ShadowType.ETCHED_OUT;
		var frame_3 = new Frame (null);
		frame_3.shadow_type = ShadowType.ETCHED_OUT;

		var logical_colour = 0;
		colour_0_panel = new ColourPanel (logical_colour, palette_data);
		colour_0_panel.tooltip_markup = 
						@"Logical colour $logical_colour.\n" +
						"Click to select it as the draw colour.\n" + 
						"Right click to select it as the erase colour.\n" +
						"Double click to change its physical colour.";
        
		logical_colour = 1;
		colour_1_panel = new ColourPanel (logical_colour, palette_data);
		colour_1_panel.tooltip_markup = 
						@"Logical colour $logical_colour.\n" +
						"Click to select it as the draw colour.\n" + 
						"Right click to select it as the erase colour.\n" +
						"Double click to change its physical colour.";

		logical_colour = 2;
		colour_2_panel = new ColourPanel (logical_colour, palette_data);
		colour_2_panel.tooltip_markup = 
						@"Logical colour $logical_colour.\n" +
						"Click to select it as the draw colour.\n" + 
						"Right click to select it as the erase colour.\n" +
						"Double click to change its physical colour.";

		logical_colour = 3;
		colour_3_panel = new ColourPanel (logical_colour, palette_data);
		colour_3_panel.tooltip_markup = 
						@"Logical colour $logical_colour.\n" +
						"Click to select it as the draw colour.\n" + 
						"Right click to select it as the erase colour.\n" +
						"Double click to change its physical colour.";

		colour_0_panel.button_press_event.connect (on_colour_0_panel_clicked);
		colour_1_panel.button_press_event.connect (on_colour_1_panel_clicked);
		colour_2_panel.button_press_event.connect (on_colour_2_panel_clicked);
		colour_3_panel.button_press_event.connect (on_colour_3_panel_clicked);

		spin_button = new SpinButton.with_range (1d, 128d, 1d);
		var spin_button_hbox = new HBox (false, 0);
		var spin_button_alignment = new Alignment (0.5f, 0.5f, 0.0f, 1.0f);
		spin_button_hbox.pack_start (spin_button, false, false, 0);
		spin_button_alignment.add (spin_button_hbox);
		spin_button.tooltip_markup = "Drawing area size (in character rows).";
		spin_button.value_changed.connect (on_spin_button_changed);

		frame_0.add (colour_0_panel);
		frame_1.add (colour_1_panel);
		frame_2.add (colour_2_panel);
		frame_3.add (colour_3_panel);        

		colour_0_hbox.pack_start (frame_0, false, false, 0);
		colour_1_hbox.pack_start (frame_1, false, false, 0);
		colour_2_hbox.pack_start (frame_2, false, false, 0);
		colour_3_hbox.pack_start (frame_3, false, false, 0);      

		colour_0_alignment.add (colour_0_hbox);
		colour_1_alignment.add (colour_1_hbox);
		colour_2_alignment.add (colour_2_hbox);
		colour_3_alignment.add (colour_3_hbox); 

		colour_vbox.pack_start (show_colour_frame, false, false, 0); 
		colour_vbox.pack_start (colour_0_alignment, false, false, 0);          
		colour_vbox.pack_start (colour_1_alignment, false, false, 0);
		colour_vbox.pack_start (colour_2_alignment, false, false, 0);
		colour_vbox.pack_start (colour_3_alignment, false, false, 0);
		colour_vbox.pack_start (spin_button_alignment, false, false, 0);

		/* Editor panel */
		var editor_panel_frame = new Frame (null);
		editor_panel_frame.shadow_type = ShadowType.ETCHED_OUT;
		var editor_panel_scrolled_window = new ScrolledWindow (null, null);
		editor_panel_scrolled_window.hscrollbar_policy = PolicyType.NEVER;
		editor_panel_scrolled_window.vscrollbar_policy = PolicyType.AUTOMATIC;
		editor_panel = new EditorPanel (edit_mode, grid_state, 
										sprite_data, palette_data,
										paint_colours);
		editor_panel_scrolled_window.add_with_viewport (editor_panel);
		editor_panel_frame.add (editor_panel_scrolled_window);

		/* Preview image */
		var preview_image_scrolled_window = new ScrolledWindow (null, null);
		preview_image_scrolled_window.hscrollbar_policy = PolicyType.NEVER;
		preview_image_scrolled_window.vscrollbar_policy = PolicyType.AUTOMATIC;
		preview_image = new PreviewImage (sprite_data, palette_data);
		preview_image_scrolled_window.add_with_viewport (preview_image);

		/* Pack three sections of main window */
		hbox.pack_start (colour_vbox, false, false, 4);
		hbox.pack_start (editor_panel_frame, false, false, 4);
		hbox.pack_start (preview_image_scrolled_window, false, false, 4);
  
		var vbox = new VBox (false, 4);
		vbox.pack_start (menu_bar, false, false, 0);
		vbox.pack_start (hbox, true, true, 0);

		add (vbox);

		tool_box = new DrawingTools (this, edit_mode);
		tool_box.delete_event.connect (on_tool_box_close);
		tool_box.delete_event.connect (tool_box.hide_on_delete);
		edit_mode.changed.connect (on_edit_mode_changed);

		sprite_data.will_change.connect(() => history.add ());

		sprite_data.changed.connect(() => { 
			spin_button.set_value (sprite_data.rows);
			is_unsaved = true; });

		window_state_event.connect (on_state_changed);

		/* Register the window as a drag and drop destination. */
		drag_dest_set (this, destination_flags, targets, actions);

		/* Set up a handler for 'drop' events. */
		drag_data_received.connect (on_drag_data_received);

		delete_event.connect (on_quit_activated);
	}

	/**
	 * Called when file is dragged into the main window.
	 *
	 * Fixes Issue 4.
	 */
	private void on_drag_data_received (Gdk.DragContext context,
	                                    int x, int y,
	                                    SelectionData selection_data,
	                                    uint target_type, uint time) {
		var uris = selection_data.get_uris ();

		/* We can only cope with one file, so only handle the first file of
		   group of files dragged into the Monster Designer window. */
		var drag_file = File.new_for_uri (uris[0]);

		/* Not interested in non-existent files or in directories. */
		if (!drag_file.query_exists () || 
				drag_file.query_file_type (0) == FileType.DIRECTORY) {
			return;
		}

        /* Check if the user wishes to save unsaved work. */
		if (is_unsaved) {
			var message = "<big><b>Save changes to document "
							 + "\"" + current_file_name () + "\""
							 + " before opening a new file?</b></big>";
			var button_text = "Open _without Saving";
			var response = save_check_dialog (message, button_text);

			switch (response) {
				case ResponseType.YES:
					/* Save. */
					save();
					break;
				case ResponseType.NO:
					/* Do nothing. */
					break;
				case ResponseType.CANCEL:
					/* Leave function without opening new file. */
					return;
			}
		}

		/* Attempt to load the file dragged into the window. */
		file = drag_file;
		load_file ();
	}

	/**
	 * Called when Open... is selected from the File menu.
	 */
	private void on_open_activated () {
        /* First check if the user wishes to save unsaved work. */
		if (is_unsaved) {
			var message = "<big><b>Save changes to document "
							 + "\"" + current_file_name () + "\""
							 + " before opening a new file?</b></big>";
			var button_text = "Open _without Saving";
			var response = save_check_dialog (message, button_text);

			switch (response) {
				case ResponseType.YES:
					/* Save. */
					save();
					break;
				case ResponseType.NO:
					/* Do nothing. */
					break;
				case ResponseType.CANCEL:
					/* Leave function without opening new file. */
					return;
			}
		}
			
		/* Call generic open function. */
	    open ();
	}

	/**
	 * Called when Save is selected from the File menu.
	 */
    private void on_save_activated () {
		if (!file.query_exists () ||
				file.query_file_type (0) == FileType.DIRECTORY) {
			/* Display Save dialog box. */
			var save_box = new SaveFileDialog (file);
			save_box.set_transient_for (this);
			save_box.do_overwrite_confirmation = true;
			var save_outcome = save_box.run ();
			if (save_outcome == ResponseType.ACCEPT) {
				file = save_box.get_file ();
			}
			save_box.destroy ();
			if (save_outcome != ResponseType.ACCEPT) {
				return;
			}
		}

		/* Call generic save function. */
		save();
	}

	/**
	 * Called when Save As... is selected from the File menu.
	 */
	private void on_save_as_activated () {
		/* If the file exists, cut it off the end of the file path */
		var save_box = new SaveFileDialog (file);
		if(file.query_file_type (0) != FileType.DIRECTORY) {
			save_box = new SaveFileDialog (file.get_parent ());
		}
		save_box.set_transient_for (this);
		save_box.do_overwrite_confirmation = true;
		var save_outcome = save_box.run ();
		if (save_outcome == ResponseType.ACCEPT) {
			file = save_box.get_file ();
		}
		save_box.destroy ();

		/* Call generic save function. */
		if (save_outcome == ResponseType.ACCEPT) {
			save();
		}
	}

	/**
	 * Returns the current file name in a format that's safe to be placed
	 * in a widget.
	 *
	 * @return current filename as GIO UTF-8 "display text".
	 */
	private string current_file_name () {
		var file_name = file.get_basename ();
		var file_exists = file.query_exists ();	
		if (file_exists) {
			try {
				var file_info = file.query_info ("*", FileQueryInfoFlags.NONE);
				file_name = file_info.get_display_name ();
			} catch (Error e) {
				stderr.printf (e.message);
			}
		}
		return file_name;
	}

	/**
	 * Called to see if the user wants to save a file before it is closed.
	 *
	 * Displays a warning alert offering the user the chance to save
	 * their work before the file is closed.
	 * 
	 * @param message dialog box message (pango formatted)
	 * @param button_text text that will appear on the dialog No button
	 * @return dialog response
	 */
	private int save_check_dialog (string message,	string button_text) {
		var result = new MessageDialog.with_markup (this, 
									DialogFlags.MODAL | 
									DialogFlags.DESTROY_WITH_PARENT,
									MessageType.WARNING, 
									ButtonsType.NONE,
									message);
		result.secondary_text = "Your changes will be lost if you don't save them.";
		result.add_button (button_text, ResponseType.NO);
		result.add_button (STOCK_CANCEL, ResponseType.CANCEL);
		result.add_button (STOCK_SAVE, ResponseType.YES);
		result.set_default_response (ResponseType.YES);

		var response = result.run ();
		result.destroy ();

		return response;
	}

	/**
	 * Common routine called when a file is to be opened.
	 */
	private void open () {
		/* Display Open dialog box */
		var open_box = new OpenFileDialog (file);
		open_box.set_transient_for (this);
		var open_outcome = open_box.run ();
		if (open_outcome == ResponseType.ACCEPT) {
			file = open_box.get_file ();
		}
		open_box.destroy ();

		/* Load file */
		if (open_outcome == ResponseType.ACCEPT) {		
			load_file ();				
		}
	}

	/**
	 * Loads the file stored in the file object.
	 */
	private void load_file () {
		try {
			/* Determine length of file */
			var file_info = file.query_info ("*", FileQueryInfoFlags.NONE);
			var length = (int) file_info.get_size ();

			if (length % 24 != 0) {
				throw new IOError.FILE_NOT_MONSTER_SPRITES
					("Not a valid Monster sprites file.");
			}

			/* Read sprite data */
			uint8[] bbc_data = {};
			var file_stream = file.read ();
		    var data_stream = new DataInputStream (file_stream);

			for (int i = 0; i != length; ++i) {
				bbc_data += data_stream.read_byte ();
			}

			/* File successfully read, try and parse it. */
			sprite_data.set_from_bbc_data (bbc_data);
			set_open ();				
		} catch (Error e) {
			var message = "<big><b>There was a problem with the file "
							 + "\"" + current_file_name () + "\""
							 + ".</b></big>";
			file_error_dialog (message, e.message);
			return;
		}
	}
	

	/**
	 * Converts from internal byte array to Beeb format, and saves to the
	 * file specified previously in the Save or Save As... dialog.
	 */
    private void save ()
    {
		try {
			/* You cannot create a file that already exists.*/
			if (file.query_exists () && 
					file.query_file_type (0) != FileType.DIRECTORY) {
				file.delete ();
			}
			
			var file_stream = file.create (FileCreateFlags.REPLACE_DESTINATION);
			var data_stream = new DataOutputStream (file_stream);

			var bbc_data = sprite_data.get_as_bbc_data ();
			var length = bbc_data.length;

			for (int i = 0; i != length; ++i) {
				data_stream.put_byte (bbc_data[i]);
			}
			is_unsaved = false;
			set_form_title ();
		} catch (Error e) {
			var message = "<big><b>There was a problem saving the file "
							 + "\"" + current_file_name () + "\""
							 + ".</b></big>";
			file_error_dialog (message, e.message);
		}
    }

	/**
	 * Standard Error Alert Dialog box displayed if there is a file error.
	 * 
	 * @param message dialog message (pango formatted)
	 * @param secondary_text secondary text (pango formatted)
	 */
	public void file_error_dialog (string message, string secondary_text) {
		var alert = new MessageDialog.with_markup (this, 
									DialogFlags.MODAL | 
									DialogFlags.DESTROY_WITH_PARENT,
									MessageType.ERROR, 
									ButtonsType.OK,
									message);
		alert.secondary_text = secondary_text;
		alert.run ();
		alert.destroy ();
	}

	/**
	 *  Set the form to a closed state, i.e. without any file open.
	 */
	public void set_closed () {
		is_unsaved = false;
		is_closed = true;
		set_form_title ();

		/* Hide all child controls, apart from the menu bar. */
        hbox.hide_all();
		tool_box.hide_all ();

		/* Grey out unavailable menu options. */
		file_save.set_sensitive (false);
		file_save_as.set_sensitive (false);
		file_close.set_sensitive (false);
		edit_menu_button.hide ();
		select_menu_button.hide ();
		mode_menu_button.hide ();
		view_menu_button.hide ();

    }

	/**
	 * When a new sprite is created, or an existing one is loaded,
	 * call this method to set up the form ready for editing.
	 */
    private void set_open()
    {
		/* Set the number of sprite rows available.
		   An event is attached to the NumericUpDown control which is 
		   called when Value is changed, like here.

		   It automatically adjusts the dimensions of the editing panel and 
		   preview panel, accordingly. */
		//spin_button.set_value (sprite_data.rows);

		/* Show all the child controls. */
		hbox.show_all ();

		/* Enable available menu options */
		file_save.set_sensitive (true);
		file_save_as.set_sensitive (true);
		file_close.set_sensitive (true);
		edit_menu_button.show_all ();
		select_menu_button.show_all ();
		mode_menu_button.show_all ();
		view_menu_button.show_all ();

		/* Display the toolbox if it is enabled. */
		if (view_tool_box.active == true) {
			tool_box.show_all ();
		}

		/* Set as saved. */
		is_unsaved = false;

		/* Set as open.*/
		is_closed = false;

		/* Set up the redo/undo for the sprite_data. */
		history = new History (sprite_data);

		/* Update title bar - must be done after is_closed has been set.*/
		set_form_title ();
    }

	/**
	 * Sets the form title, according to the currently loaded file and 
	 * whether there is unsaved work.
	 */
	private void set_form_title () {
		var title_text = "";

		if (!is_closed) {
			/* Add asterisk. */
			if (is_unsaved) {
				title_text += "*";
			}
			
			/* Add file name. */
			title_text += current_file_name ();

			/* Add add file path. */
			if (file.query_exists ()) {
				var path = file.get_parent ();
				var path_name = path.get_parse_name ();
				title_text += " (" + path_name + ") ";		
			}

			/* Add dash */
			title_text += " - ";
		}

		/* Add program name. */
		title_text += "Onslaught Monster Designer";
		title = title_text;
	}

	/**
	 * Event called when colour_0_panel is clicked.
	 *
	 * @param event Gdk.EventButton instance used to determine click type
	 */
	private bool on_colour_0_panel_clicked (Gdk.EventButton event) {
		if (event.type == Gdk.EventType.2BUTTON_PRESS) {
			change_palette (0, event);
		}

		if (event.type == Gdk.EventType.BUTTON_PRESS) {
			change_colour (0, event);
		}

		return false;
	}

	/**
	 * Event called when colour_1_panel is clicked.
	 *
	 * @param event Gdk.EventButton instance used to determine click type
	 */
	private bool on_colour_1_panel_clicked (Gdk.EventButton event) {
		if (event.type == Gdk.EventType.2BUTTON_PRESS) {
			change_palette (1, event);
		}

		if (event.type == Gdk.EventType.BUTTON_PRESS) {
			change_colour (1, event);
		}

		return false;
	}

	/**
	 * Event called when colour_2_panel is clicked.
	 *
	 * @param event Gdk.EventButton instance used to determine click type
	 */
	private bool on_colour_2_panel_clicked (Gdk.EventButton event) {
		if (event.type == Gdk.EventType.2BUTTON_PRESS) {
			change_palette (2, event);
		}

		if (event.type == Gdk.EventType.BUTTON_PRESS) {
			change_colour (2, event);
		}

		return false;
	}

	/**
	 * Event called when colour_3_panel is clicked.
	 *
	 * @param event Gdk.EventButton instance used to determine click type
	 */
	private bool on_colour_3_panel_clicked (Gdk.EventButton event) {
		if (event.type == Gdk.EventType.2BUTTON_PRESS) {
			change_palette (3, event);
		}

		if (event.type == Gdk.EventType.BUTTON_PRESS) {
			change_colour (3, event);
		}

		return false;
	}

	/**
	 * Common method which handles a ColourPanel being clicked.
	 *
	 * @param logical_colour identifies which panel was clicked
	 * @param event Gdk.EventButton instance used to determine which
	 *				mouse button was clicked
	 */
    private void change_colour (int logical_colour, Gdk.EventButton event) {
		switch (event.button) {
			case MouseButton.LEFT:
				paint_colours.draw_colour = logical_colour;
				break;
			case MouseButton.RIGHT:
				paint_colours.erase_colour = logical_colour;
				break;
		}
	}

	/**
	 * Common method which handles palette changes for all 4 colours.
	 *
	 * @param logical_colour identifies which colour should change
	 * @param event Gdk.EventButton instance used to determine which
	 *				mouse button was clicked
	 */
	private void change_palette (int logical_colour, Gdk.EventButton event) {
		switch (event.button) {
            case MouseButton.LEFT:
				palette_data.increment_colour (logical_colour);
				break;

			case MouseButton.RIGHT:
				palette_data.decrement_colour (logical_colour);
				break;
		}
	}      

	/**
	 * Event called when New is clicked from the File menu.
	 */
    private void on_new_activated () {
        /* First check whether the user wants to lose unsaved work. */
		if (is_unsaved) {
			var message = "<big><b>Save changes to "
							+ "\"" + current_file_name () + "\""
							+ " before creating a new file?</b></big>";
			var button_text = "New _without Saving";
			var response = save_check_dialog (message, button_text);

			switch (response) {
				case ResponseType.YES:
					/* Save. */
					save();
					break;
				case ResponseType.NO:
					/* Do nothing. */
					break;
				case ResponseType.CANCEL:
					/* Leave function without making new file. */
					return;
			}
		}

		/* Clear the existing filename with a new non-extant file name. */
		var parent = File.new_for_path (Environment.get_user_special_dir 
											(UserDirectory.DOCUMENTS));

		var i = 0;
		do {
			i ++;
			file = parent.get_child (@"Untitled Document $i");
		} while (file.query_exists ());

		/* Set up a default 3 row high sprite. */
		sprite_data.set_blank (3);
        set_open();
    }

	/**
	 * Event called when the Select All menu item is activated.
	 */
	private void on_select_all_activated () {
		edit_mode.mode = Mode.SELECT;
		editor_panel.do_select_all ();
	}

	/**
	 * Event called when the Select None menu item is activated.
	 */
	private void on_select_none_activated () {
		editor_panel.do_select_none ();
	}

	/**
	 * Event called when the Draw radio menu item is activated.
	 */
	private void on_draw_radio_toggled () {
		tool_box.draw_tool.active = mode_draw.active;
	}

	/**
	 * Event called when the Fill radio menu item is activated.
	 */
	private void on_fill_radio_toggled () {
		tool_box.fill_tool.active = mode_fill.active;
	}

	/**
	 * Event called when the Insert Row radio menu item is activated.
	 */
	private void on_insert_radio_toggled () {
		tool_box.insert_tool.active = mode_insert.active;
	}

	/**
	 * Event called when the Delete Row radio menu item is activated.
	 */
	private void on_delete_radio_toggled () {
		tool_box.delete_tool.active = mode_delete.active;
	}

	/**
	 * Event called when the Select radio menu item is activated.
	 */
	private void on_select_radio_toggled () {
		tool_box.select_tool.active = mode_select.active;
	}

	/**
	 * Event called when the Paste radio menu item is activated.
	 */
	private void on_paste_radio_toggled () {
		tool_box.paste_tool.active = mode_paste.active;
	}

	/**
	 * Signal handler called when edit mode changed.
 	 */
	private void on_edit_mode_changed () {
		switch (edit_mode.mode) {
			case Mode.DRAW:
				if (!mode_draw.active) {
					mode_draw.active = true;
				}
				break;
			case Mode.FILL:
				if (!mode_fill.active) {
					mode_fill.active = true;
				}
				break;
			case Mode.INSERT_ROW:
				if (!mode_insert.active) {
					mode_insert.active = true;
				}
				break;
			case Mode.DELETE_ROW:
				if (!mode_delete.active) {
					mode_delete.active = true;
				}
				break;
			case Mode.SELECT:
				if (!mode_select.active) {
					mode_select.active = true;
				} 
				break;
			case Mode.PASTE:
				if (!mode_paste.active)	{
					mode_paste.active = true;
				} 
				break;
		}
	}

	/**
	 * Event called when spin button value changed.
	 */
	private void on_spin_button_changed () {
		sprite_data.resize (spin_button.get_value_as_int ());
	}

	/**
	 * Event called when Edit menu copy option activated.
	 */
	private void on_copy_activated () {
		editor_panel.add_to_clipboard ();
	}

	/**
	 * Event called when Edit menu paste option activated.
	 */
	private void on_paste_activated () {
		edit_mode.mode = Mode.PASTE;
	}

	/** 
	 * Event called when Select menu undo option activated.
	 */
	private void on_undo_activated () {
		history.undo ();
	}

	/**
	 * Event called when Select menu redo option activated.
	 */
	private void on_redo_activated () {
		history.redo ();
	}

	/**
	 * Event called when Select menu flip horizontally option activated.
	 */
	private void on_flip_horizontal_activated () {
		editor_panel.flip_horizontal ();
	}

	/**
	 * Event called when Select menu flip vertically option activated.
	 */
	private void on_flip_vertical_activated () {
		editor_panel.flip_vertical ();
	}

	/**
	 * Event called when view grid option toggled in view menu.
	 */
	private void on_grid_activated () {
		grid_state.enabled = view_grid.active;
	}

	/**
	 * Event called when Close is clicked from the File menu.
	 */
	private void on_close_activated () {
		/* No unsaved work, so just close. */
		if (!is_unsaved) { 
			set_closed ();
			return;
		}

		/* Unsaved work, so display dialog. */
		var message = "<big><b>Save changes to "
						+ "\"" + current_file_name () + "\""
						+ " before closing the file?</b></big>";
		var button_text = "Close _without Saving";
		var response = save_check_dialog (message, button_text);

		switch (response) {
			case ResponseType.YES:
				/* Save and close. */
				save ();
				set_closed ();
				break;
			case ResponseType.NO:
				/* Just close. */
				set_closed ();
				break;
			case ResponseType.CANCEL:
				/* Do nothing. */
				break;
		}
	}

	/**
	 * Event called when Quit selected from File Menu.
	 *
	 * Emits a Gtk.Widget delete_event signal if the quit option is
	 * selected from the file menu. This means it works in exactly the
	 * same way as the close button on the window menu bar, and can be
	 * handled using the window menu bar close button's event handler.
	 */
	private void on_file_quit_activated () {
		delete_event (new Gdk.Event(Gdk.EventType.DELETE));
	}

	/**
	 * Event called when close icon clicked on the title bar.
	 *
	 * Called by the Gtk.Widget delete_event signal. Note that, in common
	 * with Gedit, no Save dialog box is displayed if you click Save - the
	 * file is just saved and the program quits.
	 *
	 * @return If we want the program to quit then false, otherwise true.
	 */
	private bool on_quit_activated () {

		/* No unsaved work, so just quit. */
		if (!is_unsaved) { 
			Gtk.main_quit (); 
			return false;
		}

		/* Unsaved work, so display dialog. */
		var message = "<big><b>Save changes to "
						+ "\"" + current_file_name () + "\""
						+ " before closing?</b></big>";
		var button_text = "Close _without Saving";
		var response = save_check_dialog (message, button_text);

		/* Save if required. */
		if (response == ResponseType.YES) {
			save ();
		}

		/* Continue execution or quit the program. */
		if (response == ResponseType.CANCEL) {
			/* Continue execution. */
			return true;
		} else {
			/* Quit. */
			Gtk.main_quit ();
			return false;
		}
	}

	/**
	 * Called when tool box close button activated. 
	 * The method hides the toolbox using its active property.
	 *
	 * @ return false to cancel the toolbox's destroy event
	 */     
	private bool on_tool_box_close (Gdk.Event e) {
		view_tool_box.active = false;
		return false;
	}

	/**
	 * Called when About... is selected from the Help menu.
	 */
	private void on_about_activated () {
		var about_dialog = new Credits (this);
		about_dialog.run ();
		about_dialog.destroy ();
	}

	private void on_tool_box_activated () {           
		if (view_tool_box.active) {
			tool_box.show_all ();
		} else {
			tool_box.hide ();
		}
	}

	private bool on_state_changed (Gdk.EventWindowState e) {
		if (view_tool_box.active == true) {
			if ((bool) e.changed_mask & Gdk.WindowState.ICONIFIED) {
				if ((bool) e.new_window_state & Gdk.WindowState.ICONIFIED) {
					tool_box.hide ();
				} else {
					tool_box.show_all ();
				}
			}
		}
		return true;
	}
}
